﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using MetaCollection.Framework.Model;

namespace MetaCollection.Framework.Change
{
	/// <summary>
	/// Calculates the differences between two items and exposes those differences
	/// as a set of IItemChanges.  This class is intended for use in the SetItem
	/// type requests to determine exactly what the user is trying to change so 
	/// action can be taken to allow, deny or perhaps rollback (in whole or part)
	/// the item changes.
	/// </summary>
	public class ItemChangeCollection : List<IItemChange>, IItemChange
	{
		/// <summary>
		/// An item change collection representing no changes.
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists", Justification="This is supposed to be an ItemChangeCollection not a generic one.")]
		public static ItemChangeCollection NoChanges { get { return new ItemChangeCollection(null, null, LoadOptions.None); } }
		/// <summary>
		/// The name of the property associated with this change(set).  For the room this is "IItemInstance"
		/// </summary>
		private string m_PropertyName;
		/// <summary>
		/// The original item.
		/// </summary>
		private IItemInstance m_Source;
		/// <summary>
		/// The new version of the item.
		/// </summary>
		private IItemInstance m_Item;
		/// <summary>
		/// LoadOption used to specify which changes we are going to consider in calculating this change collection.
		/// </summary>
		/// <remarks>This value is only set on the root collection.  All of the children collections will have a value of "None"</remarks>
		private LoadOptions m_CreationOption;

		/// <summary>
		/// Initializes a new instance of the <see cref="ItemChangeSet"/> class.
		/// </summary>
		/// <param name="source">The sourceItem (original) item </param>
		/// <param name="item">The changedItem to compare against the sourceItem</param>
		public ItemChangeCollection(IItemInstance changedItem, IItemInstance sourceItem, LoadOptions opt)
		{
			m_Source = sourceItem;
			m_Item = changedItem;
			m_PropertyName = "IItemInstance";
			m_CreationOption = opt;

			foreach (PropertyInfo prop in typeof(IItemInstance).GetProperties())
			{
				LoadOptions propertyLoadOption = (LoadOptions)Enum.Parse(typeof(LoadOptions), prop.Name);
				if (opt.IsSet(propertyLoadOption)) {
					if (prop.Name.Equals("Properties") || prop.Name.Equals("AccessClaims") || prop.Name.Equals("Relationships"))
						this.Add(new ItemChangeCollection(changedItem, sourceItem, prop));
					else
						this.Add(new ItemChange(changedItem, sourceItem, prop));
				}
			}
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="ItemChangeCollection"/> class. This internal
		/// constructor is used when nesting a collection within the parent ItemChangeCollection
		/// </summary>
		/// <param name="prop">The property which contains the set of objects to compare.</param>
		/// <param name="sourceValue">The source property value.</param>
		/// <param name="itemProperty">The item property value.</param>
		ItemChangeCollection(IItemInstance changedItem, IItemInstance sourceItem, PropertyInfo prop)
		{
			m_Source = sourceItem;
			m_Item = changedItem;
			if (prop == null)
				throw new ArgumentNullException("prop");
			if (!prop.PropertyType.IsGenericType && typeof(List<>).IsAssignableFrom(prop.PropertyType.GetGenericTypeDefinition()))
				throw new ArgumentException("Property must be an derived from List<T>", "prop");
			m_PropertyName = "IItemInstance." + prop.Name;

			Type elementType = typeof(object);
			IDictionary sourceCollection = CreateDictionaryFromInstance(prop.Name, (m_Source == null ? null : prop.GetValue(m_Source, null)), ref elementType);
			IDictionary changedCollection = CreateDictionaryFromInstance(prop.Name, (m_Item == null ? null : prop.GetValue(m_Item, null)), ref elementType);

			// create a list of all unique keys in source and target collections.
			List<string> keys = new List<string>((IEnumerable<string>)sourceCollection.Keys);
			keys = new List<string>(keys.Union((IEnumerable<string>)changedCollection.Keys));

			// create a list of each property in our elementType (as determined by CreateDictionaryFromInstance using the property name.
			PropertyInfo[] valueProperties = elementType.GetProperties();
			foreach (string key in keys)
			{
				bool sourceHasKey = ((IEnumerable<string>)sourceCollection.Keys).Any(k => String.Equals(k, key, StringComparison.OrdinalIgnoreCase));
				bool targetHasKey = ((IEnumerable<string>)changedCollection.Keys).Any(k => String.Equals(k, key, StringComparison.OrdinalIgnoreCase));

				// If item hasn't been added or removed from the collection calculate changes for the subprops.
				// Else add an ItemChange for the adding/deletion of an item and ignore subproperties since we have nothing to check them against.
				if (targetHasKey && sourceHasKey)
				{
					foreach (PropertyInfo subProp in valueProperties)
					{
						this.Add(new ItemChange(m_Item, m_Source, subProp, prop, key));//modify or no change
					}
				}
				else if (targetHasKey && !sourceHasKey)
				{
					this.Add(new ItemChange(m_Item, null, prop, key)); //add new
				}
				else
				{
					this.Add(new ItemChange(null, m_Source, prop, key));//delete
				}
			}
		}

		/// <summary>
		/// Creates a dictionary from the collection in <paramref name="property"/> by using
		/// the name in <paramref name="propertyName"/> to determine the proper cast and key
		/// selection criteria.
		/// </summary>
		/// <param name="propertyName">Name of the property.</param>
		/// <param name="property">The property.</param>
		/// <param name="elementType">Type of the value elements in the dictionary.</param>
		/// <returns>A dictionary for the collection in <paramref name="property"/>.</returns>
		private static IDictionary CreateDictionaryFromInstance(string propertyName, object property, ref Type elementType)
		{
			if (property == null)
				return new Dictionary<string, object>();

			if (propertyName.Equals("Properties"))
			{
				elementType = typeof(IItemProperty);
				return (property as IEnumerable<IItemProperty>).ToDictionary((p) => p.KeyName);
			}
			else if (propertyName.Equals("AccessClaims"))
			{
				elementType = typeof(IItemClaim);
				return (property as IEnumerable<IItemClaim>).ToDictionary((p) => string.Format(CultureInfo.CurrentCulture, "{0}-{1}-{2}", p.Id, p.ClaimType, p.ClaimValue));
			}
			else if (propertyName.Equals("Relationships"))
			{
				elementType = typeof(IItemRelationship);
				return (property as IEnumerable<IItemRelationship>).ToDictionary((p) => string.Format(CultureInfo.CurrentCulture, "{0}-{1}", p.RelationshipType, p.TargetItem));
			}
			else
				throw new ArgumentException("An unsupported property was encountered - ." + propertyName);
		}

		/// <summary>
		/// Gets the options used to create this change collection.
		/// </summary>
		/// <remarks>If a an option is not set then the value in the items is not considered in the list of changes.
		/// Do not rely on the state of values not selected in this set of options with respect to the value being 
		/// changed or not.</remarks>
		/// <value>The options specifying which field/property changes to detect</value>
		public LoadOptions Options { get { return m_CreationOption; } }

		#region IItemChange Members

		/// <summary>
		/// Gets the changes.
		/// </summary>
		/// <returns>a list of itemchanges</returns>
		public IList<IItemChange> Changes
		{
			get
			{
				List<IItemChange> changes = new List<IItemChange>();
				foreach (IItemChange change in this)
				{
					changes.AddRange(change.Changes);
				}
				return changes;
			}
		}

		/// <summary>
		/// Gets the type of the change.
		/// </summary>
		/// <value>The type of the change.</value>
		public ItemChangeTypes ChangeType
		{
			get
			{
				ItemChangeTypes changeType = ItemChangeTypes.None;

				// check for a top level change type... if none applies then calculate from children.
				if (PropertyName.Equals("IItemInstance") && (m_Source == null) && (m_Item != null))
					changeType = ItemChangeTypes.Create;
				else if (PropertyName.Equals("IItemInstance") && (m_Source != null) && (m_Item == null))
					changeType = ItemChangeTypes.Delete;
				else
				{
					foreach (IItemChange change in this)
					{
						changeType = changeType | change.ChangeType;
					}
				}
				return changeType;
			}
		}

		/// <summary>
		/// Reverts this change.
		/// </summary>
		public void Revert()
		{
			foreach (var change in this)
			{
				change.Revert();
			}
		}

		/// <summary>
		/// Gets the name of the property associated with this change.
		/// </summary>
		/// <value>The name of the property.</value>
		public string PropertyName
		{
			get { return m_PropertyName; }
		}

		/// <summary>
		/// Gets the description.
		/// </summary>
		/// <value>The description of this item change.</value>
		public string Description
		{
			get
			{
				ItemChangeTypes thisChange = this.ChangeType;
				if (thisChange == ItemChangeTypes.None)
					return "No Changes Made to " + PropertyName;
				else
				{
					List<string> subChanges = new List<string>();
					foreach (IItemChange change in this)
					{
						if (change.ChangeType != ItemChangeTypes.None)
							subChanges.Add(change.Description);
					}
					return String.Format(CultureInfo.CurrentCulture, "{0} Change Made to {1} ({2})", thisChange, PropertyName, string.Join(", ", subChanges.ToArray()));
				}
			}
		}
		/// <summary>
		/// Applies this change to a given item instance
		/// </summary>
		/// <param name="item">the instance to apply the change to</param>
		public void ApplyTo(IItemInstance item)
		{
			foreach (var change in this)
			{
				if (change.ChangeType != ItemChangeTypes.None)
					change.ApplyTo(item);
			}
		}

		public override string ToString()
		{
			return Description;
		}

		#endregion
	}
}
